package com.beforedawn.run;

import com.beforedawn.judge.Report;
import com.beforedawn.judge.ReportState;
import com.beforedawn.judge.TestNode;
import com.beforedawn.util.JudgeSystem;
import com.beforedawn.util.RunData;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 运行程序
 * 并且从inputDir读取输入数据
 * 并将数据输出到outputDir中
 *
 * @author moyulingjiu
 * @date 2021年11月25日
 */
public class Run {
    RunCommand runCommand;

    public Run(RunCommand runCommand) {
        this.runCommand = runCommand;
    }

    public Report handle(String filepath, String inputDir, String outputDir, RunData runData) {
        File input = new File(inputDir);
        File[] inputFiles = input.listFiles();
        if (inputFiles == null) {
            return new Report(ReportState.SE);
        }
        List<File> validFiles = new ArrayList<>();
        Map<String, File> outFiles = new HashMap<>();
        for (File inputFile : inputFiles) {
            if (!inputFile.isDirectory()) {
                if (inputFile.toString().endsWith(".in")) {
                    validFiles.add(inputFile);
                } else if (inputFile.toString().endsWith(".out")) {
                    outFiles.put(inputFile.toString().replace("out", "in"), inputFile);
                }
            }
        }
        if (validFiles.size() == 0) { // 没有输入的题目
            return runWithoutInput(filepath, outFiles, outputDir, runData);
        } else { // 既有输入又有输出的题目
            return run(filepath, validFiles, outFiles, outputDir, runData);
        }
    }

    private String getCommand(String filepath) {
        return runCommand.getRunCommand().replaceAll("filename", filepath);
    }

    private long getOutputSize(File file) throws IOException {
        long outputSize = 0;
        String line;
        BufferedReader outReader = new BufferedReader(new FileReader(file));
        while ((line = outReader.readLine()) != null) {
            outputSize += line.length(); // 获取输入长度
        }
        outputSize *= 2; // 不能超过标准输出的两倍长度，这是最大长度
        outReader.close();
        return outputSize;
    }

    private Report runWithoutInput(String filepath, Map<String, File> outFiles, String outputDir, RunData runData) {
        Report report = new Report();
        try {
            if (outFiles.size() != 1)
                return new Report(ReportState.SE);
            long outputSize = 0;
            for (String s : outFiles.keySet()) {
                outputSize = getOutputSize(outFiles.get(s)); // 样例输出
            }

            TestNode node = new TestNode("1");
            BufferedWriter writer = new BufferedWriter(new FileWriter(outputDir + "/1.out"));
            long start = System.currentTimeMillis();
            execCmd(node, getCommand(filepath), null, writer, outputSize, runData);
            long end = System.currentTimeMillis();
            node.setRunData(new RunData(end - start, 0));
            report.addTestNode(node);
            writer.close();
            return report;
        } catch (IOException e) {
            e.printStackTrace();
            return new Report(ReportState.SE);
        }
    }

    private Report run(String filepath, List<File> validFiles, Map<String, File> outFiles, String outputDir, RunData runData) {
        Report report = new Report();
        try {
            report.setState(ReportState.WA);
            for (File validFile : validFiles) {
                TestNode node = new TestNode(validFile.getName());
                // 读写缓冲区
                if (!outFiles.containsKey(validFile.toString())) { // 如果没有对应的文件肯定系统错误了
                    report.setState(ReportState.SE);
                    return report;
                }
                long outputSize = getOutputSize(outFiles.get(validFile.toString())); // 样例输出

                BufferedReader reader = new BufferedReader(new FileReader(validFile));
                String outputPath;
                if (validFile.toString().contains("\\")) {
                    outputPath = outputDir + validFile.toString().substring(validFile.toString().lastIndexOf("\\")).replace("in", "out");
                } else {
                    outputPath = outputDir + validFile.toString().substring(validFile.toString().lastIndexOf("/")).replace("in", "out");
                }
                BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath));
                execCmd(node, getCommand(filepath), reader, writer, outputSize, runData);
                reader.close();
                writer.close();
                report.addTestNode(node);
                if (node.getState() == ReportState.AC)
                    report.setState(ReportState.AC);
            }
        } catch (IOException e) {
            e.printStackTrace();
            report.setState(ReportState.SE);
        }
        return report;
    }

    private static class Runner extends Thread {
        private final Process process;
        private volatile int exitCode = -1;
        private volatile boolean completed = false;
        private final BufferedReader reader;
        private final BufferedWriter writer;
        private final long outputSize;
        private volatile ReportState reportState;

        private Runner(Process process, BufferedReader reader, BufferedWriter writer, long outputSize) {
            this.process = process;
            this.reader = reader;
            this.writer = writer;
            this.outputSize = outputSize;
            reportState = ReportState.AC;
        }

        @Override
        public void run() {
            try {
                sleep(100); // 等待0.1s让监控程序跑起来
                String line;
                long inputSize = 0;
                BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
                while (reader != null && (line = reader.readLine()) != null) {
                    bw.write(line + "\n"); // 将数据写入到进程内
                }
                bw.flush(); // 很重要！必须从缓冲区输入到进程内
                while ((line = br.readLine()) != null) {
                    writer.write(line + "\n"); // 把数据从进程内读出来
                    inputSize += line.length();
                    if (inputSize > outputSize) {
                        process.destroyForcibly(); // 强制终止运行的程序
                        reportState = ReportState.OLE;
                        exitCode = 1;
                        completed = true;
                        return;
                    }
                }
                exitCode = process.waitFor();
                completed = true;
            } catch (InterruptedException | IOException e) {
                Thread.currentThread().interrupt();
            }
        }

        public int getExitCode() {
            return exitCode;
        }

        public boolean isCompleted() {
            return completed;
        }

        public ReportState getReportState() {
            return reportState;
        }
    }

    private static class MemoryMonitor extends Thread {
        private volatile long max;
        private final long pid;

        private MemoryMonitor(long pid) {
            this.pid = pid;
            max = 0;
        }

        @Override
        public void run() {
            while (!this.isInterrupted()) {
                try {
                    ProcessBuilder processBuilder = new ProcessBuilder(getCommand().split(" "));
                    processBuilder.redirectErrorStream(true); // 重定向避免溢出
                    Process process = processBuilder.start();
                    String line;
                    BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), "gbk"));
                    while ((line = br.readLine()) != null) {
                        analysisMemory(line);
                    }
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        public long getMax() {
            return max;
        }

        private String getCommand() {
            if (JudgeSystem.isLinux()) {
                return "cat /proc/" + pid + "/status |grep VmRSS";
            } else if (JudgeSystem.isWindows()) {
                return "tasklist /fi \"pid eq " + pid + "\"";
            }
            return "";
        }

        private void analysisMemory(String line) {
            String[] strings = line.replaceAll(" +", " ")
                    .replace("\t", " ")
                    .replace(",", "")
                    .split(" ");
            if (JudgeSystem.isLinux()) {
                if (strings.length == 3) {
                    long memory = Long.parseLong(strings[1]);
                    if (memory > max)
                        max = memory;
                }
            } else if (JudgeSystem.isWindows()) {
                if (strings.length == 6) {
                    long memory = Long.parseLong(strings[4].strip());
                    if (memory > max)
                        max = memory;
                }
            }
        }
    }

    private void execCmd(TestNode node, String command, BufferedReader reader, BufferedWriter writer, long outputSize, RunData limit) throws IOException {
        ProcessBuilder processBuilder = new ProcessBuilder(command.split(" "));
        // 合并错误输出流
        processBuilder.redirectErrorStream(true);
        long start = System.currentTimeMillis();
        Process process = processBuilder.start();
        Runner runner = new Runner(process, reader, writer, outputSize);
        MemoryMonitor monitor = new MemoryMonitor(process.pid()); // 声明监控
        monitor.setPriority(Thread.MAX_PRIORITY);
        monitor.start();
        runner.start();
        try {
            runner.join(limit.getMillisecond() + 100);
            if (!runner.isCompleted()) {
                process.destroyForcibly();
                runner.interrupt();
                node.setState(ReportState.TLE); // 时超
            }
            long end = System.currentTimeMillis();
            monitor.interrupt();
            node.setRunData(new RunData(end - start - 100, monitor.getMax() / 1024));
        } catch (InterruptedException e) {
            runner.interrupt();
        }
        if (runner.getExitCode() != 0) {
            if (runner.getReportState() == ReportState.AC) // 返回值不为0，并且不是主动报错证明RE了。
                node.setState(ReportState.RE);
            else
                node.setState(runner.getReportState());
        }
    }
}
